Análisis Exploratorio de Datos: Torneo de Tenis UNAB 2025¶

Este análisis explora los datos del Torneo de Tenis UNAB 2025, realizado entre el 13 y el 27 de junio de 2025 en el Club San Albano, ubicado en Espora 4920, Burzaco.

Objetivo: realizar una limpieza, exploración y visualización inicial de los resultados para extraer conclusiones relevantes sobre el rendimiento de los jugadores.

📌 Autor: Sebastian Sanchez Bentolila

In [1]:
import pandas as pd
import plotly.express as px
import plotly.graph_objects as go
from plotly.subplots import make_subplots
import numpy as np

Limpieza y Preparación de Datos¶

En esta sección se realiza la limpieza y transformación de los datos para asegurar su calidad antes del análisis. Esto incluye:

  • Revisión de valores nulos.
  • Corrección de tipos de datos.
  • Renombrado de columnas si es necesario.
  • Filtrado de datos irrelevantes o inconsistentes.
In [2]:
# Cargar datos
import requests
from io import StringIO

url1 = "https://raw.githubusercontent.com/Sebastian-Sanchez-Bentolila/data/main/1_Torneo_Tenis_Unab/data/estudiantes.csv"
url2 = "https://raw.githubusercontent.com/Sebastian-Sanchez-Bentolila/data/main/1_Torneo_Tenis_Unab/data/resultados.csv"

estudiantes = pd.read_csv(StringIO(requests.get(url1).text))
resultados = pd.read_csv(StringIO(requests.get(url2).text))
In [3]:
estudiantes.head()
Out[3]:
Nombre Apellido Grupo Carrera Edad
0 Sebastian Sanchez Bentolila A Ciencia de Datos 20
1 Franco Nicolas Tagliaferro A Ciencia de Datos 24
2 Humberto Andrés Coria Avalos A CCC. Enseñanza de Matemáticas 35
3 Aarón Ferreira A Ciencia de Datos 22
4 Agustin Zalazar A Administración 25
In [4]:
resultados.head()
Out[4]:
Estudiante_1 Estudiante_2 Jornada Grupo Resultado
0 Lautaro Rodriguez Gaspar Mamani 1 B 0-2
1 Micaela López Milagros Lezcano 1 B 2-0
2 Hernán Correa Romina Fernández 1 B 0-2
3 Hernán Correa Lautaro Rodríguez 2 B 2-1
4 Gaspar Mamani Micaela López 2 B 2-1
In [4]:
# Procesar estadísticas 
def process_results(df):
    # Crear una copia explícita del DataFrame para evitar el warning
    df_processed = df.copy()
    
    # Separar los resultados en goles a favor y en contra
    df_processed[['GF', 'GC']] = df_processed['Resultado'].str.split('-', expand=True).astype(int)
    
    jugadores = pd.concat([df_processed['Estudiante_1'], df_processed['Estudiante_2']]).unique()
    
    stats = []
    for jugador in jugadores:
        partidos_jugados = len(df_processed[(df_processed['Estudiante_1'] == jugador) | 
                           (df_processed['Estudiante_2'] == jugador)])
        
        partidos_ganados = len(df_processed[((df_processed['Estudiante_1'] == jugador) & 
                                           (df_processed['GF'] > df_processed['GC'])) | 
                                          ((df_processed['Estudiante_2'] == jugador) & 
                                           (df_processed['GC'] > df_processed['GF']))])
        
        partidos_perdidos = partidos_jugados - partidos_ganados
        puntos = partidos_ganados * 3
        
        gf = df_processed[df_processed['Estudiante_1'] == jugador]['GF'].sum() + \
             df_processed[df_processed['Estudiante_2'] == jugador]['GC'].sum()
        
        gc = df_processed[df_processed['Estudiante_1'] == jugador]['GC'].sum() + \
             df_processed[df_processed['Estudiante_2'] == jugador]['GF'].sum()
        
        stats.append({
            'Jugador': jugador,
            'PJ': partidos_jugados,
            'PG': partidos_ganados,
            'PP': partidos_perdidos,
            'Puntos': puntos,
            'GF': gf,
            'GC': gc,
            'Dif': gf - gc
        })
    
    return pd.DataFrame(stats).sort_values('Puntos', ascending=False)

# Procesar los datos sin warnings
stats_a = process_results(resultados[resultados['Grupo'] == 'A'].copy())
stats_b = process_results(resultados[resultados['Grupo'] == 'B'].copy())

# Combinar datos para los gráficos
df_puntos = pd.concat([
    stats_a.assign(Grupo='A'),
    stats_b.assign(Grupo='B')
])

📊 Visualizaciones Interactivas¶

En esta sección se presentan las visualizaciones interactivas que permiten explorar los datos de manera dinámica para obtener conclusiones rápidas y efectivas.

Distribución de Puntos por Grupo¶

Se analiza la distribución de puntos obtenidos por los jugadores en cada grupo, comparando el rendimiento general y las diferencias entre niveles.

In [5]:
df_puntos = pd.concat([
    stats_a.assign(Grupo='A'),
    stats_b.assign(Grupo='B')
])

fig1 = px.box(df_puntos, x='Grupo', y='Puntos', 
             color='Grupo', 
             color_discrete_map={'A': '#0056b3', 'B': '#007bff'},
             title='<b>Distribución de Puntos por Grupo</b><br><i>Comparación entre grupos A y B</i>',
             hover_data=['Jugador', 'PJ', 'PG', 'PP'])

fig1.update_layout(
    plot_bgcolor='white',
    paper_bgcolor='white',
    xaxis_title='Grupo',
    yaxis_title='Puntos',
    showlegend=False,
    hovermode='closest'
)

fig1.update_traces(boxmean=True)  # Muestra la media
fig1.show()

Relación Games a Favor vs. Games en Contra¶

En esta sección se analiza la relación entre los games ganados y los games perdidos por cada jugador. Esta métrica permite evaluar el rendimiento relativo dentro de los partidos disputados.

In [6]:
fig2 = px.scatter(df_puntos, x='GF', y='GC', color='Grupo',
                 color_discrete_map={'A': '#0056b3', 'B': '#007bff'},
                 hover_name='Jugador',
                 hover_data=['Puntos', 'Dif'],
                 title='<b>Games a favor vs. games en contra</b><br><i>Jugadores de ambos grupos</i>')

# Línea de igualdad
max_val = max(df_puntos[['GF', 'GC']].max())
fig2.add_trace(
    go.Scatter(
        x=[0, max_val],
        y=[0, max_val],
        mode='lines',
        line=dict(color='gray', dash='dash'),
        name='Línea de igualdad'
    )
)

fig2.update_layout(
    plot_bgcolor='white',
    paper_bgcolor='white',
    xaxis_title='Games a favor',
    yaxis_title='Games en contra',
    legend_title='Grupo'
)

fig2.show()

Promedio de Puntos por Grupo¶

Aquí se calcula y visualiza el promedio de puntos obtenidos por los jugadores en cada grupo. Esto permite comparar el nivel general de desempeño entre los distintos grupos del torneo.

In [7]:
promedios = pd.DataFrame({
    'Grupo': ['A', 'B'],
    'Puntos': [stats_a['Puntos'].mean(), stats_b['Puntos'].mean()]
})

fig3 = px.bar(promedios, x='Grupo', y='Puntos', 
              color='Grupo',
              color_discrete_map={'A': '#0056b3', 'B': '#007bff'},
              text='Puntos',
              title='<b>Promedio de puntos por grupo</b>')

fig3.update_traces(texttemplate='%{y:.1f}', textposition='outside')
fig3.update_layout(
    plot_bgcolor='white',
    paper_bgcolor='white',
    xaxis_title='Grupo',
    yaxis_title='Puntos promedio',
    showlegend=False
)

fig3.show()

Distribución de Jugadores por Carrera¶

Se presenta la distribución de participantes según sus carreras académicas dentro de la universidad, identificando la representatividad de cada disciplina en el torneo.

In [8]:
carrera_counts = estudiantes['Carrera'].value_counts().reset_index()
carrera_counts.columns = ['Carrera', 'Cantidad']
total = carrera_counts['Cantidad'].sum()
carrera_counts['Porcentaje'] = (carrera_counts['Cantidad'] / total * 100).round(1)

fig4 = px.bar(carrera_counts, 
             x='Cantidad', 
             y='Carrera', 
             orientation='h',
             color='Cantidad',
             color_continuous_scale='Blues',
             text='Cantidad',
             title='<b>Distribución de estudiantes por carrera</b>')

fig4.update_traces(
    texttemplate='%{x} (%{customdata[0]}%)',
    customdata=carrera_counts[['Porcentaje']],
    textposition='outside'
)

fig4.update_layout(
    plot_bgcolor='white',
    paper_bgcolor='white',
    xaxis_title='Cantidad de estudiantes',
    yaxis_title='',
    coloraxis_showscale=False,
    uniformtext_minsize=8,
    uniformtext_mode='hide'
)

fig4.show()

Distribución de Jugadores por Edad¶

En esta sección se analiza la distribución etaria de los jugadores, identificando los rangos de edad más frecuentes y su participación relativa en el torneo.

In [9]:
fig5 = px.histogram(estudiantes, 
                   x='Edad',
                   nbins=12,  # Aumenté el número de bins para más detalle
                   title='<b>Distribución de Edades de los Participantes</b><br><span style="font-size:14px; color:#555">Torneo de Tenis UNaB 2025</span>',
                   color_discrete_sequence=['#1a5276'],  # Un azul más elegante
                   marginal='box',  # Cambié a box plot para mostrar estadísticas
                   hover_data=['Carrera', 'Grupo', 'Nombre'],
                   opacity=0.85,  # Transparencia para mejor visualización
                   template='plotly_white')  # Usamos un template limpio

# Personalización avanzada
fig5.update_layout(
    plot_bgcolor='rgba(248,249,250,1)',  # Fondo muy claro
    paper_bgcolor='rgba(248,249,250,1)',
    xaxis_title='<b>Edad (años)</b>',
    yaxis_title='<b>Cantidad de estudiantes</b>',
    bargap=0.15,  # Más espacio entre barras
    font=dict(
        family="Arial",
        size=12,
        color="#333"
    ),
    hoverlabel=dict(
        bgcolor="white",
        font_size=12,
        font_family="Arial"
    ),
    title_x=0.5,  # Título centrado
    title_font=dict(size=20),
    margin=dict(l=40, r=40, t=80, b=40)
)

fig5.show()

Distribución de Edades por Carrera¶

Aquí se visualiza un boxplot interactivo que muestra la distribución de edades por cada carrera, permitiendo identificar diferencias en la participación según la edad y la disciplina académica.

In [10]:
# Definimos una paleta de colores moderna y accesible
color_palette = ['#3498db', '#e74c3c', '#2ecc71', '#f39c12', '#9b59b6', '#1abc9c']

fig6 = px.box(estudiantes, 
             x='Carrera', 
             y='Edad',
             color='Carrera',
             color_discrete_sequence=color_palette,
             title='<b>Distribución de Edades por Carrera</b><br><span style="font-size:14px; color:#555">Torneo de Tenis UNaB 2025</span>',
             hover_data=['Grupo', 'Nombre'],
             points="all",  # Muestra todos los puntos
             template='plotly_white')

# Personalización avanzada
fig6.update_layout(
    plot_bgcolor='rgba(248,249,250,1)',
    paper_bgcolor='rgba(248,249,250,1)',
    xaxis_title='<b>Carrera Universitaria</b>',
    yaxis_title='<b>Edad (años)</b>',
    showlegend=False,
    font=dict(
        family="Arial",
        size=12,
        color="#333"
    ),
    hoverlabel=dict(
        bgcolor="white",
        font_size=12,
        font_family="Arial"
    ),
    title_x=0.5,
    title_font=dict(size=20),
    margin=dict(l=50, r=50, t=100, b=80),
    height=600  # Altura aumentada para mejor visualización
)

# Personalización de los boxes
fig6.update_traces(
    marker=dict(
        size=4,
        opacity=0.7,
        line=dict(width=1, color='DarkSlateGrey')
    ),
    line=dict(width=2),
    boxmean=True,  # Muestra la media
    hovertemplate="<b>Carrera:</b> %{x}<br>" +
                 "<b>Edad:</b> %{y} años<br>" +
                 "<extra></extra>"
)

# Rotación de etiquetas y estilo del eje X
fig6.update_xaxes(
    tickangle=45,
    tickfont=dict(size=12),
    showgrid=False
)

# Estilo del eje Y
fig6.update_yaxes(
    gridcolor='rgba(200,200,200,0.2)',
    showline=True,
    linecolor='black'
)

# Añadir línea de promedio general
avg_age = estudiantes['Edad'].mean()
fig6.add_hline(y=avg_age, 
              line_dash="dot", 
              line_color="#7f8c8d",
              annotation_text=f'Promedio general: {avg_age:.1f} años', 
              annotation_position="bottom right")

fig6.show()

💾 Guardado de Gráficos Generados¶

En esta etapa se guardan los gráficos generados durante el análisis para su posterior inclusión en informes, presentaciones y documentación del proyecto.

In [11]:
import plotly.express as px

try:
    fig1.write_html("output/fig1.html")
    fig2.write_html("output/fig2.html")
    fig3.write_html("output/fig3.html")
    fig4.write_html("output/fig4.html")
    fig5.write_html("output/fig5.html")
    fig6.write_html("output/fig6.html")
    print("Guardado con exito!")
except:
    print("Fallo al guardar los gráficos interactivos con Plotly")
Guardado con exito!